/*******************************************************************************
 * Copyright (c) 2006, 2016 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.concurrent;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.eclipse.cdt.dsf.internal.DsfPlugin;
import org.eclipse.cdt.dsf.internal.LoggingUtils;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/**
 * Default implementation of a DSF executor interfaces, based on the 
 * standard java.util.concurrent.ThreadPoolExecutor.
 * 
 * @since 1.0
 */

public class DefaultDsfExecutor extends ScheduledThreadPoolExecutor 
    implements DsfExecutor 
{
    /**
     * Instance counter for DSF executors.  Used in the executor's thread name.
     */
    private static int fgInstanceCounter = 0;
    
    /**
     * Name of the executor, used in the executor's thread name.
     */
    private String fName;
    
    /** Thread factory that creates the single thread to be used for this executor */
    static class DsfThreadFactory implements ThreadFactory {
        private String fThreadName; 
        DsfThreadFactory(String name) {
            fThreadName = name;
        }
        
        Thread fThread;
        @Override
        public Thread newThread(Runnable r) {
            assert fThread == null;  // Should be called only once.
            fThread = new Thread(new ThreadGroup(fThreadName), r, fThreadName, 0); 
            return fThread;
        }
    }

    public DefaultDsfExecutor() {
        this("DSF Executor"); //$NON-NLS-1$
    }
    
    /** 
     * Creates a new DSF Executor with the given name.
     * @param name Name used to create executor's thread.
     */
    public DefaultDsfExecutor(String name) {
        super(1, new DsfThreadFactory(name + " - " + fgInstanceCounter++)); //$NON-NLS-1$
        fName = name;
        
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            // If tracing, pre-start the dispatch thread, and add it to the map.
            prestartAllCoreThreads();
            fThreadToExecutorMap.put(((DsfThreadFactory)getThreadFactory()).fThread, DefaultDsfExecutor.this);
        }
    }
    
    @Override
    public boolean isInExecutorThread() {
        return Thread.currentThread().equals( ((DsfThreadFactory)getThreadFactory()).fThread );
    }

    /**
	 * @since 2.1
	 */
    public int getCurrentExecutionDepth() {
    	if (fCurrentlyExecuting != null) {
    		return fCurrentlyExecuting.fDepth;
    	}
        return -1;
    }

    protected String getName() { 
        return fName;
    }
    
    static void logException(Throwable t) {
        DsfPlugin plugin = DsfPlugin.getDefault();
        if (plugin == null) return;
        
        ILog log = plugin.getLog();
        if (log != null) {
            log.log(new Status(
                IStatus.ERROR, DsfPlugin.PLUGIN_ID, -1, "Uncaught exception in DSF executor thread", t)); //$NON-NLS-1$
        }                   
        // Print out the stack trace to console if assertions are enabled. 
        if(ASSERTIONS_ENABLED) {
            ByteArrayOutputStream outStream = new ByteArrayOutputStream(512);
            PrintStream printStream = new PrintStream(outStream);
            try {
                printStream.write("Uncaught exception in session executor thread: ".getBytes()); //$NON-NLS-1$
            } catch (IOException e2) {}
            t.printStackTrace(new PrintStream(outStream));
            System.err.println(outStream.toString());
        }
    }
    
    //
    // Utilities used for tracing.
    //
    protected static boolean DEBUG_EXECUTOR = false;
    protected static String DEBUG_EXECUTOR_NAME = ""; //$NON-NLS-1$
    protected static boolean ASSERTIONS_ENABLED = false;
    static {
        DEBUG_EXECUTOR = DsfPlugin.DEBUG && Boolean.parseBoolean(
            Platform.getDebugOption("org.eclipse.cdt.dsf/debug/executor")); //$NON-NLS-1$
        DEBUG_EXECUTOR_NAME = DsfPlugin.DEBUG 
            ? Platform.getDebugOption("org.eclipse.cdt.dsf/debug/executorName") : ""; //$NON-NLS-1$ //$NON-NLS-2$
        assert (ASSERTIONS_ENABLED = true) == true;
    }  

    /** 
     * This map is used by DsfRunnable/Query/DsfCallable to track by which executor
     * an executable object was created.
     * <br>Note: Only used when tracing. 
     */
    static Map<Thread, DefaultDsfExecutor> fThreadToExecutorMap = new HashMap<Thread, DefaultDsfExecutor>();
    
    /** 
     * Currently executing runnable/callable.
     * <br>Note: Only used when tracing. 
     */
    TracingWrapper fCurrentlyExecuting;
    
    /** 
     * Counter number saved by each tracing runnable when executed 
     * <br>Note: Only used when tracing. 
     */
    int fSequenceCounter;

    /** 
     * Wrapper for runnables/callables, is used to store tracing information 
     * <br>Note: Only used when tracing. 
     */
    abstract class TracingWrapper {
        /** Sequence number of this runnable/callable */
        int fSequenceNumber = -1; 
        int fDepth = 0;
        
        /** Trace of where the runnable/callable was submitted to the executor */
        StackTraceWrapper fSubmittedAt = null; 
        
        /** Reference to the runnable/callable that submitted this runnable/callable to the executor */
        TracingWrapper fSubmittedBy = null;

		/**
		 * The names of the executor submitter methods we support, ordered by
		 * popularity so as to optimize the tracing logic. (For the curious,
		 * 'execute' is by far the most commonly called--ten times more often
		 * than 'submit', in fact).
		 */
        private final String[] SUBMITTER_METHOD_NAMES = {
        	"execute", "submit", "schedule", "scheduleAtFixedRate", "scheduleWithFixedDelay" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
        
		/**
		 */
        TracingWrapper() {
        	
			// Get the this thread's stack trace and then search for the call
			// into the executor's submitter method. We'll want to ignore
			// everything up to and including that call.
        	StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        	int frameIgnoreCount = 1;
        	String executorClassName = this.getClass().getEnclosingClass().getSimpleName();	// e.g., "DefaultDsfExecutor"
        	outer: for (StackTraceElement frame : stackTrace) {
        		final String framestr = frame.toString();
        		for (String methodName : SUBMITTER_METHOD_NAMES) {
	        		if (framestr.contains(executorClassName + "." + methodName + "(")) { //$NON-NLS-1$ //$NON-NLS-2$
	        			break outer;	// exit both loops
	        		}
        		}
    			frameIgnoreCount++;
        	}
        	
        	if (frameIgnoreCount == stackTrace.length) {
				// Internal error, really. We were unable to identify the
				// executor's submission function. Our check above must be
				// overlooking a possibility
        		frameIgnoreCount = 0;
        	}
        	
        	// guard against the offset being greater than the stack trace
        	frameIgnoreCount = Math.min(frameIgnoreCount, stackTrace.length);
        	fSubmittedAt = new StackTraceWrapper(new StackTraceElement[stackTrace.length - frameIgnoreCount]);
            if (fSubmittedAt.fStackTraceElements.length > 0) {
	            System.arraycopy(stackTrace, frameIgnoreCount, fSubmittedAt.fStackTraceElements, 0, fSubmittedAt.fStackTraceElements.length);
            }
            
            if (isInExecutorThread() &&  fCurrentlyExecuting != null) {
                fSubmittedBy = fCurrentlyExecuting;
            }
        }
        
        void traceExecution() {
            fSequenceNumber = fSequenceCounter++;
            fDepth = fSubmittedBy == null ? 0 : fSubmittedBy.fDepth + 1;
            fCurrentlyExecuting = this;

            // Write to console only if tracing is enabled (as opposed to tracing or assertions).
            if (DEBUG_EXECUTOR && ("".equals(DEBUG_EXECUTOR_NAME) || fName.equals(DEBUG_EXECUTOR_NAME))) { //$NON-NLS-1$
                StringBuilder traceBuilder = new StringBuilder();
    
                // Record the time
                traceBuilder.append(DsfPlugin.getDebugTime());
                traceBuilder.append(' ');
    
                // Record the executor #
                traceBuilder.append("DSF execution #"); //$NON-NLS-1$
                traceBuilder.append(fSequenceNumber);

                // Record the executor name
                traceBuilder.append(". Executor is ("); //$NON-NLS-1$
                traceBuilder.append(((DsfThreadFactory)getThreadFactory()).fThreadName);
                traceBuilder.append(')');

				// This will be a Runnable or a Callable. Hopefully it will also
				// be a DsfExecutable and thus be instrumented with trace/debug
				// information. In nearly every case, it will be an anonymous
				// inner class.
                final Object executable = getExecutable();
                
				// Append executable class name. The anonymous inner class name
				// name won't be very interesting; use the parent class instead.
                traceBuilder.append("\n\tExecutable detail: \n\t\ttype = "); //$NON-NLS-1$
                Class<? extends Object> execClass = executable.getClass();
                traceBuilder.append(execClass.isAnonymousClass() ? execClass.getSuperclass().getName() : execClass.getName());
                
                // Append the executable reference
                final String refstr = LoggingUtils.toString(executable, false);
                String tostr = LoggingUtils.trimTrailingNewlines(executable.toString());
                traceBuilder.append("\n\t\t"); //$NON-NLS-1$
                traceBuilder.append("instance = ").append(refstr); //$NON-NLS-1$
                if (!tostr.equals(refstr)) {
                	traceBuilder.append(" ["); //$NON-NLS-1$
                	traceBuilder.append(tostr);
                	traceBuilder.append(']');                	
                }

				// Determine if the created-at and submitted-at information is
				// the same. If so, consolidate.
                StackTraceElement[] createdAtStack = null;
                StackTraceElement[] submittedAtStack = (fSubmittedAt == null) ? null : fSubmittedAt.fStackTraceElements;
                int createdBySeqNum = Integer.MIN_VALUE;
                int submittedBySeqNum = (fSubmittedBy == null) ? Integer.MIN_VALUE : fSubmittedBy.fSequenceNumber;
                if (executable instanceof DsfExecutable) {
                    DsfExecutable dsfExecutable = (DsfExecutable)executable; 
                    createdAtStack = (dsfExecutable.fCreatedAt == null) ? null : dsfExecutable.fCreatedAt.fStackTraceElements;
                    createdBySeqNum = (dsfExecutable.fCreatedBy == null) ? Integer.MIN_VALUE : dsfExecutable.fCreatedBy.fSequenceNumber;
                }

                boolean canConsolidate = false;
                if ((createdBySeqNum == submittedBySeqNum) && (createdAtStack != null) && (submittedAtStack != null)) {
                	if ((createdAtStack.length == submittedAtStack.length) ||
                		(createdAtStack.length >=3 && submittedAtStack.length >= 3)) {
                		
                		canConsolidate = true;
                		int count = Math.min(createdAtStack.length, 3);
                		for (int i = 0; i < count; i++) {
                			if (createdAtStack[i].toString().compareTo(submittedAtStack[i].toString()) != 0) {
                				canConsolidate = false;
                				break;
                			}
                		}
                	}
                }

                if (canConsolidate) {
            		traceBuilder.append("\n\t\tcreated and submitted"); //$NON-NLS-1$
                    if (createdBySeqNum != Integer.MIN_VALUE) {
                        traceBuilder.append(" by #"); //$NON-NLS-1$
                        traceBuilder.append(createdBySeqNum);
                    }
                    if (createdAtStack != null) {
                        traceBuilder.append(" at:"); //$NON-NLS-1$
                        for (int i = 0; i < createdAtStack.length && i < 3; i++) {
                            traceBuilder.append("\n\t\t\t"); //$NON-NLS-1$
                            traceBuilder.append(createdAtStack[i].toString());
                        }
                    }
                }
                else {
	                // Append "create by" info.
                    if (createdAtStack != null || createdBySeqNum != Integer.MIN_VALUE) {
                        traceBuilder.append("\n\t\tcreated  "); //$NON-NLS-1$
                        if (createdBySeqNum != Integer.MIN_VALUE) {
                            traceBuilder.append(" by #"); //$NON-NLS-1$
                            traceBuilder.append(createdBySeqNum);
                        }
                        if (createdAtStack != null) {
                            traceBuilder.append(" at:"); //$NON-NLS-1$
                            for (int i = 0; i < createdAtStack.length && i < 3; i++) {
                                traceBuilder.append("\n\t\t\t"); //$NON-NLS-1$
                                traceBuilder.append(createdAtStack[i].toString());
                        }   
                    }
                }
    
                // Submitted info
                traceBuilder.append("\n\t\tsubmitted"); //$NON-NLS-1$
                if (fSubmittedBy != null) {
                    traceBuilder.append(" by #"); //$NON-NLS-1$
                    traceBuilder.append(fSubmittedBy.fSequenceNumber);
                }
                traceBuilder.append(" at:"); //$NON-NLS-1$
                for (int i = 0; i < fSubmittedAt.fStackTraceElements.length && i < 3; i++) {
                    traceBuilder.append("\n\t\t\t"); //$NON-NLS-1$
                    traceBuilder.append(fSubmittedAt.fStackTraceElements[i].toString());
                }
                }
                                
                // Finally write out to console
                DsfPlugin.debug(traceBuilder.toString());
            }
        }

        abstract protected Object getExecutable();
    }
    
    
    class TracingWrapperRunnable extends TracingWrapper implements Runnable {
        final Runnable fRunnable;
        
        
        public TracingWrapperRunnable(Runnable runnable) {
            if (runnable == null) throw new NullPointerException();
            fRunnable = runnable;

            // Check if executable wasn't executed already.
            if (DEBUG_EXECUTOR && fRunnable instanceof DsfExecutable) {
                assert !((DsfExecutable)fRunnable).getSubmitted() : "Executable was previously executed."; //$NON-NLS-1$
                ((DsfExecutable)fRunnable).setSubmitted();
            }
        }

        @Override
        protected Object getExecutable() { return fRunnable; }
        
        @Override
        public void run() {
            traceExecution();

            // Finally invoke the runnable code.
            try {
                fRunnable.run();
            } catch (RuntimeException e) {
                // If an exception was thrown in the Runnable, trace it.  
                // Because there is no one else to catch it, it is a 
                // programming error.
                logException(e);
                throw e;
            } catch (Error e) {
                logException(e);
                throw e;
            }
        }
    }
    
    public class TracingWrapperCallable<T> extends TracingWrapper implements Callable<T> {
        final Callable<T> fCallable;
        
        /**
         * @deprecated use constructor that takes just the Callable parameter
         */
        @Deprecated
		public TracingWrapperCallable(Callable<T> callable, int frameIgnoreCount) {
            if (callable == null) throw new NullPointerException();
            fCallable = callable;
        }
        
        /**
		 * @since 2.1
		 */
        public TracingWrapperCallable(Callable<T> callable) {
            if (callable == null) throw new NullPointerException();
            fCallable = callable;
        }

        @Override
        protected Object getExecutable() { return fCallable; }

        @Override
        public T call() throws Exception {
            traceExecution();
            
            // Finally invoke the runnable code.
            // Note that callables can throw exceptions that can be caught
            // by clients that invoked them using ExecutionException.
            return fCallable.call();
        }
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            if ( !(callable instanceof TracingWrapper) ) {
                callable = new TracingWrapperCallable<V>(callable);
            }
        }
        return super.schedule(callable, delay, unit);
    }
     @Override
     public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
         if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
             if ( !(command instanceof TracingWrapper) ) {
                 command = new TracingWrapperRunnable(command);
             }
         }
         return super.schedule(command, delay, unit);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            command = new TracingWrapperRunnable(command);
        }
        return super.scheduleAtFixedRate(command, initialDelay, period, unit);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            command = new TracingWrapperRunnable(command);
        }
        return super.scheduleWithFixedDelay(command, initialDelay, delay, unit);
    }
    
    @Override
    public void execute(Runnable command) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            command = new TracingWrapperRunnable(command);
        }
        super.execute(command);
    }     
    
    @Override
    public Future<?> submit(Runnable command) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            command = new TracingWrapperRunnable(command);
        }
        return super.submit(command);
    }
    
    @Override
    public <T> Future<T> submit(Callable<T> callable) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            callable = new TracingWrapperCallable<T>(callable);
        }
        return super.submit(callable);
    }
    
    @Override
    public <T> Future<T> submit(Runnable command, T result) {
        if(DEBUG_EXECUTOR || ASSERTIONS_ENABLED) {
            command = new TracingWrapperRunnable(command);
        }
        return super.submit(command, result);
    }
    
    @Override
	public void shutdown() {
    	if (DEBUG_EXECUTOR && ("".equals(DEBUG_EXECUTOR_NAME) || fName.equals(DEBUG_EXECUTOR_NAME))) { //$NON-NLS-1$    		
    		DsfPlugin.debug(DsfPlugin.getDebugTime() + " Executor (" + ((DsfThreadFactory)getThreadFactory()).fThreadName + ") is being shut down. Already submitted tasks will be executed, new ones will not.");	 //$NON-NLS-1$ //$NON-NLS-2$
    	}
    	super.shutdown();
    }

    @Override
	public List<Runnable> shutdownNow() {
    	if (DEBUG_EXECUTOR && ("".equals(DEBUG_EXECUTOR_NAME) || fName.equals(DEBUG_EXECUTOR_NAME))) { //$NON-NLS-1$
    		DsfPlugin.debug(DsfPlugin.getDebugTime() + " Executor (" + ((DsfThreadFactory)getThreadFactory()).fThreadName + ") is being shut down. No queued or new tasks will be executed, and will attempt to cancel active ones.");	 //$NON-NLS-1$ //$NON-NLS-2$
    	}
    	return super.shutdownNow();
    }
    
    @Override
    protected void terminated() {
    	fThreadToExecutorMap.remove(((DsfThreadFactory)getThreadFactory()).fThread);
    	super.terminated();
    }
}
